Completed
Pull Request — master (#71)
by Marcelo
35s
created

build.js ➔ addToZip   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
c 2
b 0
f 0
nc 1
nop 3
dl 0
loc 3
rs 10
1
import path from 'path';
2
import Zip from 'jszip';
3
import Promise, { all, promisifyAll, reject, resolve } from 'bluebird';
4
import {
5
    complement,
6
    concat,
7
    contains,
8
    curry,
9
    drop,
10
    equals,
11
    endsWith,
12
    filter,
13
    head,
14
    identity,
15
    ifElse,
16
    is,
17
    join,
18
    lensProp,
19
    map,
20
    mapObjIndexed,
21
    merge,
22
    over,
23
    propEq,
24
    replace,
25
    sort,
26
    startsWith,
27
    subtract,
28
    takeWhile,
29
    test,
30
    tryCatch,
31
    unary,
32
    union,
33
    without
34
} from 'ramda';
35
import deepmerge from 'deepmerge';
36
import { emitSuccess, emitWarning } from './input';
37
import { getProperties } from './vm';
38
import { compileModulesFromSource, ensureNoImports, inspect } from './module';
39
40
const fs = promisifyAll(require('fs'));
41
42
const defaultFileOptions = { date: new Date(1149562800000) };
43
const requiredFiles = ['package.json', 'index.js'];
44
45
const localeByFile = drop(8)
46
    & takeWhile(complement(equals('.')))
47
    & join('');
48
49
/**
50
 * Converts a list of locale files to pairs containing locale string and content
51
 *
52
 * @param {String[]} localeFiles
53
 * @return {Promise}
54
 */
55
function localesToPairs(localeFiles) {
56
    return all(localeFiles.map(localeFile => fs.readFileAsync(localeFile, 'utf-8')
57
        .then(JSON.parse)
58
        .then(json => [localeByFile(localeFile), json])));
59
}
60
61
/**
62
 * Projects locale for each translatable subfield
63
 *
64
 * @param {String} locale
65
 * @param {Object} config
66
 * @return {Object}
67
 */
68
const project = curry((locale, config) => ({
69
    title: { [locale]: config.title },
70
    description: { [locale]: config.description },
71
    preview: { [locale]: config.preview },
72
    params: mapObjIndexed(param => merge(param,
73
        { description: { [locale]: param.description } }), config.params)
74
}));
75
76
/**
77
 * Lazily runs the extension using all possible listed locales and extracts
78
 * the meta-data.
79
 *
80
 * @param {String} source
81
 * @param {[(String, *)]} locales
82
 * @return {Promise}
83
 */
84
const runInAllLocales = curry((source, locales) =>
85
    compileModulesFromSource(source).then(modules =>
86
        all([['default', {}], ...locales].map(([locale, strings]) =>
87
            getProperties({ name: `precompile-${locale}`, source }, strings, modules)
88
                .then(project(locale))))
89
                .then(ifElse(propEq('length', 1), head, unary(deepmerge.all)))));
90
91
/**
92
 * Creates a meta file where the information about precompilation is stored
93
 *
94
 * @param {Object} locales
95
 * @return {Promise}
96
 */
97
function createMetaFile(locales) {
98
    return fs.writeFileAsync('.meta', JSON.stringify(locales));
99
}
100
101
/**
102
 * Precompiles linked files, generating a .meta file with all the meta data
103
 *
104
 * @param {Object<String, String[]>} { code, files }
0 ignored issues
show
Documentation introduced by
The parameter { does not exist. Did you maybe forget to remove this comment?
Loading history...
105
 * @return {Promise}
106
 */
107
function precompile({ code, files }) {
108
    return resolve(files)
109
        .then(filter(test(/^locales(\/|\\)[a-z]{2,3}(_[A-Z]{2})?\.json$/)))
110
        .then(localesToPairs)
111
        .then(runInAllLocales(code))
112
        .then(createMetaFile)
113
        .thenReturn(['.meta', ...files]);
114
}
115
116
/**
117
 * Ensures there are missing no files in order to a allow a basic compilation
118
 * and filter the used modules. It also warns about possible improvements in the
119
 * extensions
120
 *
121
 * @param {String[]} files
122
 * @return {Promise}
123
 */
124
function filterFiles(files) {
125
    const clearModule = replace(/^\.\//, '');
126
    const resources = files | filter(test(/^((icon\.png)|(README(\.\w+)?\.md))$/));
127
    const missing = without(files, requiredFiles);
128
129
    if (missing.length > 0) {
130
        return reject(Error(`missing ${missing.join(', ')} from the project`));
131
    }
132
133
    if (!contains('icon.png', files)) {
134
        emitWarning('compiling extension without providing an icon.png file');
135
    }
136
137
    return fs.readFileAsync('index.js', 'utf-8')
138
        .then(inspect)
139
        .then(over(lensProp('modules'), filter(startsWith('./'))))
140
        .then(({ code, modules }) => ({
141
            code, files: union(modules.map(clearModule), concat(resources, requiredFiles)) }));
142
}
143
144
/**
145
 * Returns all the files in a directory if it exists. Otherwise, return an
146
 * empty array as fallback (everything inside a promise)
147
 *
148
 * @param {String} directory
149
 * @return {String[]}
150
 */
151
function listFiles(directory) {
152
    return fs.lstatAsync(directory)
153
        .then(lstat => lstat.isDirectory() ? fs.readdirAsync(directory) : [])
154
        .catchReturn([]);
155
}
156
157
/**
158
 * Links autocomplete files
159
 *
160
 * @return {Promise}
161
 */
162
function linkAutoComplete() {
163
    return listFiles('autocomplete')
164
        .then(filter(endsWith('.js')) & map(path.join('autocomplete', _)))
0 ignored issues
show
Bug introduced by
The variable _ seems to be never declared. If this is a global, consider adding a /** global: _ */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
165
        .tap(files => all(files.map(file => fs.readFileAsync(file)
166
            .then(ensureNoImports(file)))));
167
}
168
169
/**
170
 * Links locale files
171
 *
172
 * @return {Promise}
173
 */
174
function linkLocales() {
175
    return listFiles('locales')
176
        .then(filter(test(/^[a-z]{2}(_[A-Z]{2,3})?\.json$/)) & map(path.join('locales', _)))
0 ignored issues
show
Bug introduced by
The variable _ seems to be never declared. If this is a global, consider adding a /** global: _ */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
177
        .filter(location => fs.readFileAsync(location)
178
            .then(JSON.parse & is(Object))
179
            .catchReturn(false))
180
        .catchReturn([]);
181
}
182
183
/**
184
 * Links the files to precompilation, including locales and autocomplete
185
 * scripts. For autocomplete files, ensuring it is a valid script without
186
 * requires. For locales, filtering true locale files and appending the full
187
 * qualified name for current files.
188
 *
189
 * @param {Object<String, String[]>} { code, files }
0 ignored issues
show
Documentation introduced by
The parameter { does not exist. Did you maybe forget to remove this comment?
Loading history...
190
 * @return {Promise}
191
 */
192
function linkFiles({ code, files }) {
193
    return all([linkLocales(), linkAutoComplete()])
194
        .spread(union)
195
        .then(union(files) & sort(subtract) & (files => ({ code, files })));
196
}
197
198
/**
199
 * Opens package.json and extrats its contents. Returns a promise containing
200
 * the file list to be zipped and the package.json content parsed
201
 *
202
 * @param {String} dir
203
 * @return {Promise}
204
 */
205
function getProjectName(dir) {
206
    return fs.readFileAsync(path.join(dir, 'package.json'))
207
        .then(JSON.parse & _.name)
0 ignored issues
show
Bug introduced by
The variable _ seems to be never declared. If this is a global, consider adding a /** global: _ */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
208
        .catchThrow(new Error('Failed to parse package.json from the project'));
209
}
210
211
/**
212
 * Generates a zip package using a node buffer containing the necessary files
213
 *
214
 * @param {String} dir
215
 * @param {String[]} files
216
 * @param {String} name
0 ignored issues
show
Documentation introduced by
The parameter name does not exist. Did you maybe forget to remove this comment?
Loading history...
217
 */
218
const createZip = curry((dir, files) => {
219
    const zip = new Zip();
220
    files.forEach(filename => {
221
        zip.file(filename, fs.readFileSync(path.join(dir, filename)), defaultFileOptions);
222
    });
223
    return zip;
224
});
225
226
/**
227
 * Taking account the -o parameter can be used to specify the output directory,
228
 * let's deal with it
229
 *
230
 * @param {String} customPath
231
 * @param {String} filename
232
 * @return {String}
233
 */
234
function resolveOutputTarget(customPath, filename) {
235
    const realPath = path.resolve('.', customPath);
236
237
    const getPath = tryCatch(realPath => fs.lstatSync(realPath).isDirectory()
238
        ? path.join(realPath, filename)
239
        : realPath
240
    , identity);
241
242
    return getPath(realPath);
243
}
244
245
/**
246
 * Saves the zip file from buffer to the filesystem
247
 *
248
 * @param {String} dir
249
 * @param {Zip} zip
250
 * @param {String} name
251
 */
252
const saveZip = curry((dir, zip, name) => {
253
    const target = resolveOutputTarget(dir, `${name}.rung`);
254
255
    return new Promise((resolve, reject) => {
256
        zip.generateNodeStream({ type: 'nodebuffer', streamFiles: true })
257
            .pipe(fs.createWriteStream(target))
258
            .on('error', reject)
259
            .on('finish', ~resolve(target));
260
    });
261
});
262
263
/**
264
 * Precompiles an extension and generates a .rung package
265
 *
266
 * @param {Object} args
267
 */
268
export default function build(args) {
269
    const dir = path.resolve('.', args._[1] || '');
270
271
    return fs.readdirAsync(dir)
272
        .then(filterFiles)
273
        .then(linkFiles)
274
        .then(precompile)
275
        .then(createZip(dir))
276
        .then(zip => all([zip, getProjectName(dir)]))
277
        .spread(saveZip(args.output || '.'))
278
        .tap(~emitSuccess('Rung extension compilation'));
279
}
280